<template>
  <div class="uploadBox">
    <input :disabled="status !== Status.wait" type="file" @change="handleFileChange" />
    <el-button @click="handleUpload" :disabled="uploadDisabled">上传</el-button>
    <el-button @click="handleResume" v-if="status === Status.pause"
      >恢复</el-button
    >
    <el-button
      v-else
      :disabled="status !== Status.uploading || !container.hash"
      @click="handlePause"
      >暂停</el-button
    >
    <div class="allProgress">
      <div>计算文件hash</div>
      <el-progress :percentage="hashPercentage"></el-progress>
      <div>总进度</div>
      <el-progress :percentage="uploadPercentage"></el-progress>
    </div>
    <div class="otherBox">
      <el-table :data="fileData">
        <el-table-column prop="hash" label="切片hash" align="center">
        </el-table-column>
        <el-table-column label="大小(kb)" align="center" width="120">
          <template v-slot="{ row }">
            {{ row.size }}
          </template>
        </el-table-column>
        <el-table-column label="进度" align="center">
          <template v-slot="{ row }">
            <el-progress :percentage="row.percentage" color="#909399">
            </el-progress>
          </template>
        </el-table-column>
      </el-table>
    </div>
  </div>
</template>

<script>
const SIZE = 10 * 1024 * 1024;
const Status = {
  wait: "wait",
  pause: "pause",
  uploading: "uploading",
};
export default {
  data: () => {
    return {
      Status,
      container: {
        file: null,
        hash: "",
        worker: null,
      },
      hashPercentage: 0,
      fileData: [],
      requestList: [], // 缓存的请求列表
      status: Status.wait,
      // 当暂停时会取消 xhr 导致进度条后退
      // 为了避免这种情况，需要定义一个假的进度条
      fakeUploadPercentage: 0,
    };
  },
  computed: {
    
    uploadDisabled(){
      return(
        !this.container.file || [Status.pause, Status.uploading].includes(this.status)
      )
    },
    uploadPercentage() {
      if (!this.container.file || !this.fileData.length) return 0;
      const loaded = this.fileData
        .map((item) => item.size * item.percentage)
        .reduce((acc, cur) => acc + cur);
      return parseInt((loaded / this.container.file.size).toFixed(2));
    },
  },
  watch:{
    uploadPercentage(now){
      if(now > this.fakeUploadPercentage){
        this.fakeUploadPercentage = now
      }
    }
  },
  methods: {
    handleFileChange(e) {
      const [file] = e.target.files;
      if (!file) return;
      Object.assign(this.$data, this.$options.data()); // 重置data里的数据
      this.container.file = file;
      console.log(file);
    },
    // 监听progrss事件反馈进度条
    request({
      url,
      method = "post",
      data,
      headers = {},
      onProgress = (e) => e,
      requestList, // 请求列表 中断用
    }) {
      return new Promise((resolve) => {
        const xhr = new XMLHttpRequest();
        xhr.upload.onprogress = onProgress;
        xhr.open(method, url);
        Object.keys(headers).forEach((key) => {
          xhr.setRequestHeader(key, headers[key]);
        });
        xhr.send(data);
        xhr.onload = (e) => {
          // 将请求成功的xhr从列表中删除
          if (requestList) {
            const xhrIndex = requestList.findIndex((item) => item === xhr);
            requestList.splice(xhrIndex, 1);
          }
          resolve({
            data: e.target.response,
          });
        };
        requestList?.push(xhr); // 连缀符 当前面对象存在的时候才会执行
      });
    },
    // 生成文件切片
    createFileChunk(file, size = SIZE) {
      const fileChunkList = [];
      let cur = 0;
      while (cur < file.size) {
        fileChunkList.push({ file: file.slice(cur, cur + size) });
        cur += size;
      }
      return fileChunkList;
    },
    // 上传切片 同时过滤已上传切片
    /**
     * uploadedList :已经上传的哈希列表文件
     */
    async uploadChunks(uploadedList = []) {
      const requestList = this.fileData
        .filter(({ hash }) => !uploadedList.includes(hash))
        .map(({ chunk, hash, index, fileHash }) => {
          const formData = new FormData();
          formData.append("chunk", chunk);
          formData.append("hash", hash);
          formData.append("filename", this.container.file.name);
          formData.append("fileHash", fileHash);
          return {
            formData,
            index,
          };
        })
        .map(async ({ formData, index }) => {
          this.request({
            url: "http://localhost:3000",
            data: formData,
            onProgress: this.createProgressHandler(this.fileData[index]),
            requestList: this.requestList,
          });
        });
      await Promise.all(requestList);
      // 计算切片合并 如果不相等会怎么样
      if (uploadedList.length + requestList.length === this.data.length) {
        setTimeout(() => {
          this.mergeRequest();
        }, 5000);
      }
    },
    // 切片按钮
    async handleUpload() {
      if (!this.container.file) return;
      this.status = Status.uploading;
      // 创建切片文件
      const fileChunkList = this.createFileChunk(this.container.file);
      // 生成文件哈希
      this.container.hash = await this.calculateHash(fileChunkList);
      // 判定是否拥有
      const { shouldUpload, uploadedList } = await this.verifyUpload(
        this.container.file.name,
        this.container.hash
      );
      if (!shouldUpload) {
        this.$message.success("秒传：上传成功");
        this.status = Status.wait;
        return;
      }

      // 重命名生成数组下标
      this.fileData = fileChunkList.map(({ file }, index) => ({
        chunk: file,
        fileHash: this.container.hash,
        index,
        size: file.size,
        hash: this.container.hash + "_" + index, // 文件名 + 数组下标
        percentage: uploadedList.includes(index)?100:0, // 已上传文件的进度条不清空
      }));
      console.log(this.fileData, "上传的切片文件");
      // 给所有切片创建请求列表并上传
      await this.uploadChunks(uploadedList);
    },
    // 合并切片请求
    async mergeRequest() {
      await this.request({
        url: "http://localhost:3000/merge",
        headers: {
          "content-type": "application/json",
        },
        data: JSON.stringify({
          size: SIZE,
          filename: this.container.file.name,
          hash: this.container.hash,
        }),
      });
      this.$message.success("上传成功");
      this.status = Status.wait;
    },
    // 切片进度条反馈
    createProgressHandler(item) {
      return (e) => {
        item.percentage = parseInt(String((e.loaded / e.total) * 100));
      };
    },
    // 生成文件 hash
    calculateHash(fileChunkList) {
      return new Promise((resolve) => {
        this.container.worker = new Worker("/hash.js");
        this.container.worker.postMessage({ fileChunkList });
        this.container.worker.onmessage = (e) => {
          const { percentage, hash } = e.data;
          this.hashPercentage = percentage;
          if (hash) {
            resolve(hash);
          }
        };
      });
    },
    // 验证hash是否存在
    async verifyUpload(filename, fileHash) {
      const { data } = await this.request({
        url: "http://localhost:3000/verify",
        headers: {
          "content-type": "application/json",
        },
        data: JSON.stringify({
          filename,
          fileHash,
        }),
      });
      return JSON.parse(data);
    },
    // 暂停上传
    handlePause() {
      this.status = Status.pause
      this.resetData();
    },
    resetData(){
      this.requestList.forEach((xhr) => xhr?.abort());
      this.requestList = [];
      // 清空worker
      if(this.container.worker){
        this.container.worker.onmessage = null
      }
    },
    // 恢复上传
    async handleResume() {
      this.status = Status.uploading;
      const {uploadedList} = await this.verifyUpload(this.container.file.name,this.container.hash);
      await this.uploadChunks(uploadedList);
    },
  },
};
</script>

<style>
</style>